package com.google.code.sms.controller;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeSet;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.multiaction.MultiActionController;

import com.google.code.sms.Logger;
import com.google.code.sms.command.UserSettingsCommand;
import com.google.code.sms.dao.MediaElementDao;
import com.google.code.sms.domain.MediaElement;
import static com.google.code.sms.domain.MediaElement.MediaElementType.MEDIA_DIRECTORY;
import com.google.code.sms.domain.MediaFolder;
import com.google.code.sms.domain.MediaElementIndex;
import com.google.code.sms.domain.Player;
import com.google.code.sms.domain.PlayerTechnology;
import com.google.code.sms.domain.Playlist;
import com.google.code.sms.domain.SearchCriteria;
import com.google.code.sms.domain.SearchResult;
import com.google.code.sms.domain.User;
import com.google.code.sms.service.MediaElementService;
import com.google.code.sms.service.PlayerService;
import com.google.code.sms.service.PlaylistService;
import com.google.code.sms.service.SearchService;
import com.google.code.sms.service.SecurityService;
import com.google.code.sms.service.SettingsService;
import com.google.code.sms.util.StringUtil;
import com.google.code.sms.util.XMLBuilder;

import static com.google.code.sms.security.RESTRequestParameterProcessingFilter.decrypt;
import static com.google.code.sms.util.XMLBuilder.Attribute;
import static com.google.code.sms.util.XMLBuilder.AttributeSet;

/**
 * REST API For Clients.
 * 
 * Note: Exceptions thrown from the methods are intercepted by RESTFilter.
 *
 * @author Scott Ware
 */
public class RESTController extends MultiActionController {

    private static final Logger LOG = Logger.getLogger("RESTController");
    
    private SettingsService settingsService;
    private SecurityService securityService;
    private PlayerService playerService;
    private MediaElementService mediaElementService;
    private DownloadController downloadController;
    private CoverArtController coverArtController;
    private SubtitlesController subtitlesController;
    private LeftController leftController;
    private HomeController homeController;
    private StreamController streamController;
    private HLSController hlsController;
    private PlaylistService playlistService;
    private SearchService searchService;
    private MediaElementDao mediaElementDao;

    // Test connection to server
    public void ping(HttpServletRequest request, HttpServletResponse response) throws Exception
    {
        XMLBuilder builder = createXMLBuilder(request, response, true).endAll();
        response.getWriter().print(builder);
    }

    // Returns a list a Media Folders
    public void getMediaFolders(HttpServletRequest request, HttpServletResponse response) throws Exception {
        request = wrapRequest(request);
        XMLBuilder builder = createXMLBuilder(request, response, true);
        builder.add("mediaFolders", false);

        for (MediaFolder mediaFolder : settingsService.getAllMediaFolders()) {
            AttributeSet attributes = new AttributeSet();
            attributes.add("id", mediaFolder.getId());
            attributes.add("name", mediaFolder.getName());
            builder.add("mediaFolder", attributes, true);
        }
        builder.endAll();
        response.getWriter().print(builder);
    }

    // Returns a list of alphabetical indexes (root elements) for a given media folder.
    public void getIndexes(HttpServletRequest request, HttpServletResponse response) throws Exception {
        request = wrapRequest(request);
        XMLBuilder builder = createXMLBuilder(request, response, true);

        builder.add("indexes", false);

        List<MediaFolder> mediaFolders = settingsService.getAllMediaFolders();
        Integer mediaFolderId = ServletRequestUtils.getIntParameter(request, "mediaFolderId");
        if (mediaFolderId != null) {
            for (MediaFolder mediaFolder : mediaFolders) {
                if (mediaFolderId.equals(mediaFolder.getId())) {
                    mediaFolders = Arrays.asList(mediaFolder);
                    break;
                }
            }
        }

        SortedMap<MediaElementIndex, SortedSet<MediaElementIndex.SortableMediaElementWithMediaElements>> indexedMediaElements =
                leftController.getMediaFolderContent(mediaFolders).getIndexedMediaElements();

        for (Map.Entry<MediaElementIndex, SortedSet<MediaElementIndex.SortableMediaElementWithMediaElements>> entry : indexedMediaElements.entrySet()) {
            builder.add("index", "name", entry.getKey().getIndex(), false);

            for (MediaElementIndex.SortableMediaElementWithMediaElements element : entry.getValue()) {
                for (MediaElement mediaElement : element.getMediaElements()) {
                    if (mediaElement != null) {
                        builder.add("element", true,
                                new Attribute("name", mediaElement.getName()),
                                new Attribute("id", mediaElement.getId()));
                    }
                }
            }
            builder.end();
        }

        // Add children
        List<MediaElement> singleMediaElements = leftController.getSingleMediaElements(mediaFolders);

        for (MediaElement singleMediaElement : singleMediaElements) {
            builder.add("child", createAttributesForMediaElement(singleMediaElement), true);
        }

        builder.endAll();
        response.getWriter().print(builder);
    }

    private AttributeSet createAttributesForPlaylist(Playlist playlist) {
        AttributeSet attributes;
        attributes = new AttributeSet();
        attributes.add("id", playlist.getId());
        attributes.add("name", playlist.getName());
        attributes.add("owner", playlist.getUsername());
        attributes.add("public", playlist.isPublic());
        attributes.add("songCount", playlist.getElementCount());
        attributes.add("duration", playlist.getDuration());
        attributes.add("created", StringUtil.toISO8601(playlist.getCreated()));
        return attributes;
    }

    public void getMediaElement(HttpServletRequest request, HttpServletResponse response) throws Exception {
        request = wrapRequest(request);
        XMLBuilder builder = createXMLBuilder(request, response, true);

        int id = ServletRequestUtils.getRequiredIntParameter(request, "id");
        MediaElement mediaElement = mediaElementDao.getMediaElement(id);
        if (mediaElement == null || !mediaElement.isAudio()) {
            error(request, response, ErrorCode.NOT_FOUND, "Media Element not found.");
            return;
        }
        builder.add("mediaElement", createAttributesForMediaElement(mediaElement), true);

        builder.endAll();
        response.getWriter().print(builder);
    }

    public void getMediaDirectory(HttpServletRequest request, HttpServletResponse response) throws Exception {
        request = wrapRequest(request);

        int id = ServletRequestUtils.getRequiredIntParameter(request, "id");
        MediaElement directory = mediaElementService.getMediaElement(id);
        if (directory == null) {
            error(request, response, ErrorCode.NOT_FOUND, "Directory not found");
            return;
        }

        MediaElement parent = mediaElementService.getParentOf(directory);
        AttributeSet attributes = new AttributeSet();
        attributes.add("id", id);
        
        if(parent != null)
        {
            attributes.add("parent", parent.getId());
        }
        
        attributes.add("name", directory.getName());

        XMLBuilder builder = createXMLBuilder(request, response, true);
        builder.add("directory", attributes, false);

        for (MediaElement child : mediaElementService.getChildrenOf(directory.getPath(), true, true, true, true)) {
            attributes = createAttributesForMediaElement(child);
            builder.add("child", attributes, true);
        }
        builder.endAll();
        response.getWriter().print(builder);
    }

    public void search(HttpServletRequest request, HttpServletResponse response) throws Exception {
        request = wrapRequest(request);
        XMLBuilder builder = createXMLBuilder(request, response, true);

        builder.add("searchResult", false);

        String query = request.getParameter("query");
        SearchCriteria criteria = new SearchCriteria();
        criteria.setQuery(StringUtils.trimToEmpty(query));
        criteria.setCount(ServletRequestUtils.getIntParameter(request, "audioCount", 20));
        criteria.setOffset(ServletRequestUtils.getIntParameter(request, "audioOffset", 0));
        SearchResult audioElements = searchService.search(criteria, SearchService.IndexType.AUDIO_ELEMENT);
        for (MediaElement mediaElement : audioElements.getMediaElements()) {
            builder.add("audio", true,
                    new Attribute("name", mediaElement.getName()),
                    new Attribute("id", mediaElement.getId()));
        }

        criteria.setCount(ServletRequestUtils.getIntParameter(request, "videoCount", 20));
        criteria.setOffset(ServletRequestUtils.getIntParameter(request, "videoOffset", 0));
        SearchResult videoElements = searchService.search(criteria, SearchService.IndexType.VIDEO_ELEMENT);
        for (MediaElement mediaElement : videoElements.getMediaElements()) {
            AttributeSet attributes = createAttributesForMediaElement(mediaElement);
            builder.add("video", attributes, true);
        }

        criteria.setCount(ServletRequestUtils.getIntParameter(request, "directoryCount", 20));
        criteria.setOffset(ServletRequestUtils.getIntParameter(request, "directoryOffset", 0));
        SearchResult directoryElements = searchService.search(criteria, SearchService.IndexType.DIRECTORY_ELEMENT);
        for (MediaElement mediaElement : directoryElements.getMediaElements()) {
            AttributeSet attributes = createAttributesForMediaElement(mediaElement);
            builder.add("directory", attributes, true);
        }

        builder.endAll();
        response.getWriter().print(builder);
    }

    public void getPlaylists(HttpServletRequest request, HttpServletResponse response) throws Exception {
        request = wrapRequest(request);
        XMLBuilder builder = createXMLBuilder(request, response, true);

        User user = securityService.getCurrentUser(request);
        String authenticatedUsername = user.getUsername();
        String requestedUsername = request.getParameter("username");

        if (requestedUsername == null) {
            requestedUsername = authenticatedUsername;
        } else if (!user.isAdminRole()) {
            error(request, response, ErrorCode.NOT_AUTHORIZED, authenticatedUsername + " is not authorized to get playlists for " + requestedUsername);
            return;
        }

        builder.add("playlists", false);

        for (Playlist playlist : playlistService.getReadablePlaylistsForUser(requestedUsername)) {
            List<String> sharedUsers = playlistService.getPlaylistUsers(playlist.getId());
            builder.add("playlist", createAttributesForPlaylist(playlist), sharedUsers.isEmpty());
            if (!sharedUsers.isEmpty()) {
                for (String username : sharedUsers) {
                    builder.add("allowedUser", (Iterable<Attribute>) null, username, true);
                }
                builder.end();
            }
        }

        builder.endAll();
        response.getWriter().print(builder);
    }

    public void getPlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception {
        request = wrapRequest(request);
        String username = securityService.getCurrentUsername(request);

        XMLBuilder builder = createXMLBuilder(request, response, true);

        int id = ServletRequestUtils.getRequiredIntParameter(request, "id");

        Playlist playlist = playlistService.getPlaylist(id);
        if (playlist == null) {
            error(request, response, ErrorCode.NOT_FOUND, "Playlist not found: " + id);
            return;
        }
        if (!playlistService.isReadAllowed(playlist, username)) {
            error(request, response, ErrorCode.NOT_AUTHORIZED, "Permission denied for playlist " + id);
            return;
        }
        builder.add("playlist", createAttributesForPlaylist(playlist), false);
        for (String allowedUser : playlistService.getPlaylistUsers(playlist.getId())) {
            builder.add("allowedUser", (Iterable<Attribute>) null, allowedUser, true);
        }
        for (MediaElement mediaElement : playlistService.getElementsInPlaylist(id)) {
            AttributeSet attributes = createAttributesForMediaElement(mediaElement);
            builder.add("entry", attributes, true);
        }

        builder.endAll();
        response.getWriter().print(builder);
    }

    public void createPlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception {
        request = wrapRequest(request);
        String username = securityService.getCurrentUsername(request);

        Integer playlistId = ServletRequestUtils.getIntParameter(request, "playlistId");
        String name = request.getParameter("name");
        if (playlistId == null && name == null) {
            error(request, response, ErrorCode.MISSING_PARAMETER, "Playlist ID or name must be specified.");
            return;
        }

        Playlist playlist;
        if (playlistId != null) {
            playlist = playlistService.getPlaylist(playlistId);
            if (playlist == null) {
                error(request, response, ErrorCode.NOT_FOUND, "Playlist not found: " + playlistId);
                return;
            }
            if (!playlistService.isWriteAllowed(playlist, username)) {
                error(request, response, ErrorCode.NOT_AUTHORIZED, "Permission denied for playlist " + playlistId);
                return;
            }
        } else {
            playlist = new Playlist();
            playlist.setName(name);
            playlist.setCreated(new Date());
            playlist.setChanged(new Date());
            playlist.setPublic(false);
            playlist.setUsername(username);
            playlistService.createPlaylist(playlist);
        }

        List<MediaElement> songs = new ArrayList<>();
        for (int id : ServletRequestUtils.getIntParameters(request, "songId")) {
            MediaElement song = mediaElementService.getMediaElement(id);
            if (song != null) {
                songs.add(song);
            }
        }
        playlistService.setElementsInPlaylist(playlist.getId(), songs);

        XMLBuilder builder = createXMLBuilder(request, response, true);
        builder.endAll();
        response.getWriter().print(builder);
    }

    public void updatePlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception {
        request = wrapRequest(request);
        String username = securityService.getCurrentUsername(request);

        int id = ServletRequestUtils.getRequiredIntParameter(request, "playlistId");
        Playlist playlist = playlistService.getPlaylist(id);
        if (playlist == null) {
            error(request, response, ErrorCode.NOT_FOUND, "Playlist not found: " + id);
            return;
        }
        if (!playlistService.isWriteAllowed(playlist, username)) {
            error(request, response, ErrorCode.NOT_AUTHORIZED, "Permission denied for playlist " + id);
            return;
        }

        String name = request.getParameter("name");
        if (name != null) {
            playlist.setName(name);
        }
        Boolean isPublic = ServletRequestUtils.getBooleanParameter(request, "public");
        if (isPublic != null) {
            playlist.setPublic(isPublic);
        }
        playlistService.updatePlaylist(playlist);

        List<MediaElement> elements = playlistService.getElementsInPlaylist(id);
        boolean elementsChanged = false;

        SortedSet<Integer> tmp = new TreeSet<>();
        for (int elementIndexToRemove : ServletRequestUtils.getIntParameters(request, "elementIndexToRemove")) {
            tmp.add(elementIndexToRemove);
        }
        List<Integer> elementIndexesToRemove = new ArrayList<>(tmp);
        Collections.reverse(elementIndexesToRemove);
        for (Integer songIndexToRemove : elementIndexesToRemove) {
            elements.remove(songIndexToRemove.intValue());
            elementsChanged = true;
        }
        for (int elementToAdd : ServletRequestUtils.getIntParameters(request, "elementIdToAdd")) {
            MediaElement song = mediaElementService.getMediaElement(elementToAdd);
            if (song != null) {
                elements.add(song);
                elementsChanged = true;
            }
        }
        if (elementsChanged) {
            playlistService.setElementsInPlaylist(id, elements);
        }

        XMLBuilder builder = createXMLBuilder(request, response, true);
        builder.endAll();
        response.getWriter().print(builder);
    }

    public void deletePlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception {
        request = wrapRequest(request);
        String username = securityService.getCurrentUsername(request);

        int id = ServletRequestUtils.getRequiredIntParameter(request, "id");
        Playlist playlist = playlistService.getPlaylist(id);
        if (playlist == null) {
            error(request, response, ErrorCode.NOT_FOUND, "Playlist not found: " + id);
            return;
        }
        if (!playlistService.isWriteAllowed(playlist, username)) {
            error(request, response, ErrorCode.NOT_AUTHORIZED, "Permission denied for playlist " + id);
            return;
        }
        playlistService.deletePlaylist(id);

        XMLBuilder builder = createXMLBuilder(request, response, true);
        builder.endAll();
        response.getWriter().print(builder);
    }

    public void getMediaElementList(HttpServletRequest request, HttpServletResponse response) throws Exception {
        request = wrapRequest(request);

        XMLBuilder builder = createXMLBuilder(request, response, true);
        builder.add("mediaElementList", false);

        int size = ServletRequestUtils.getIntParameter(request, "size", 10);
        int offset = ServletRequestUtils.getIntParameter(request, "offset", 0);
        size = Math.max(0, Math.min(size, 500));
        String type = ServletRequestUtils.getRequiredStringParameter(request, "type");

        List<MediaElement> elements;
        switch(type)
        {
            case "frequent":
                elements = homeController.getMostFrequent(offset, size);
                break;   
            case "recent":
                elements = homeController.getMostRecent(offset, size);
                break;
            case "newest":
                elements = homeController.getNewest(offset, size);
                break;
            case "alphabetical":
                elements = homeController.getAlphabetical(offset, size);
                break;
            default:
                throw new Exception("Invalid list type: " + type);
        }

        for (MediaElement element : elements) {
            MediaElement mediaElement = mediaElementService.getMediaElement(element.getPath());
            AttributeSet attributes = createAttributesForMediaElement(mediaElement);
            builder.add("mediaElement", attributes, true);
        }
        builder.endAll();
        response.getWriter().print(builder);
    }

    private AttributeSet createAttributesForMediaElement(MediaElement mediaElement)
    {
        MediaElement parent = mediaElementService.getParentOf(mediaElement);
        
        AttributeSet attributes = new AttributeSet();
        attributes.add("id", mediaElement.getId());
        
        if (parent != null && !settingsService.isMediaFolder(parent.getPath())) 
        {
            attributes.add("parent", parent.getId());
        }
        
        attributes.add("name", mediaElement.getName());
        attributes.add("mediaType", mediaElement.getMediaElementType().name());
        attributes.add("created", StringUtil.toISO8601(mediaElement.getCreated()));
            
        switch (mediaElement.getMediaElementType()) 
        {
            case AUDIO: case VIDEO:
                attributes.add("suffix", mediaElement.getFormat());
                attributes.add("contentType", StringUtil.getMimeType(mediaElement.getFormat()));
                attributes.add("size", mediaElement.getFileSize());
                attributes.add("path", getRelativePath(mediaElement));
                attributes.add("coverArt", findCoverArt(mediaElement, parent));
                attributes.add("duration", mediaElement.getMetaData().getDuration());
                attributes.add("bitRate", mediaElement.getMetaData().getBitRate());
                attributes.add("title", mediaElement.getMetaData().getTitle());
                attributes.add("audio_codec", mediaElement.getMetaData().getAudioCodec());
                attributes.add("audio_sample_rate", mediaElement.getMetaData().getAudioSampleRate());
                attributes.add("audio_configuration", mediaElement.getMetaData().getAudioConfiguration());
                break;

            default:
                break;
        }

        switch (mediaElement.getMediaElementType()) 
        {
            case AUDIO:
                attributes.add("album", mediaElement.getMetaData().getAlbum());
                attributes.add("artist", mediaElement.getMetaData().getArtist());
                attributes.add("albumArtist", mediaElement.getMetaData().getAlbumArtist());
                attributes.add("genre", mediaElement.getMetaData().getGenre());
                attributes.add("year", mediaElement.getMetaData().getYear());
                attributes.add("discNumber", mediaElement.getMetaData().getDiscNumber());
                attributes.add("discSubtitle", mediaElement.getMetaData().getDiscSubtitle());
                attributes.add("track", mediaElement.getMetaData().getTrackNumber());
                attributes.add("comment", mediaElement.getMetaData().getComment());
                break;

            case VIDEO:
                attributes.add("poster", findPoster(mediaElement, parent));
                attributes.add("subtitles", findSubtitles(mediaElement, parent));
                attributes.add("year", mediaElement.getMetaData().getYear());
                attributes.add("video_codec", mediaElement.getMetaData().getVideoCodec());
                attributes.add("rating", mediaElement.getMetaData().getRating());
                attributes.add("description", mediaElement.getMetaData().getDescription());
                attributes.add("tagline", mediaElement.getMetaData().getTagline());
                attributes.add("certificate", mediaElement.getMetaData().getCertificate());
                break;

            case MEDIA_DIRECTORY:
                attributes.add("year", mediaElement.getMetaData().getYear());
                attributes.add("coverArt", findCoverArt(mediaElement, parent));
                break;
                
            case DIRECTORY:
                break;

            default:
                break;
        }
        
        return attributes;
    }

    private Integer findCoverArt(MediaElement mediaElement, MediaElement parent) {
        MediaElement dir = mediaElement.isMediaDirectory() ? mediaElement : parent;
        if (dir != null && dir.getMetaData().getCoverArtPath() != null) {
            return dir.getId();
        }
        return null;
    }
    
    private Integer findPoster(MediaElement mediaElement, MediaElement parent) {
        MediaElement dir = mediaElement.isMediaDirectory() ? mediaElement : parent;
        if (dir != null && dir.getMetaData().getPosterPath() != null) {
            return dir.getId();
        }
        return null;
    }
    
    private Integer findSubtitles(MediaElement mediaElement, MediaElement parent) {
        MediaElement dir = mediaElement.isMediaDirectory() ? mediaElement : parent;
        if (dir != null && dir.getMetaData().getSubtitlesPath() != null) {
            return dir.getId();
        }
        return null;
    }

    private String getRelativePath(MediaElement musicFile) {

        String filePath = musicFile.getPath();

        // Convert slashes.
        filePath = filePath.replace('\\', '/');

        String filePathLower = filePath.toLowerCase();

        List<MediaFolder> mediaFolders = settingsService.getAllMediaFolders(false, true);
        for (MediaFolder mediaFolder : mediaFolders) {
            String folderPath = mediaFolder.getPath();
            folderPath = folderPath.replace('\\', '/');
            String folderPathLower = folderPath.toLowerCase();

            if (filePathLower.startsWith(folderPathLower)) {
                String relativePath = filePath.substring(folderPath.length());
                return relativePath.startsWith("/") ? relativePath.substring(1) : relativePath;
            }
        }

        return null;
    }

    public ModelAndView download(HttpServletRequest request, HttpServletResponse response) throws Exception {
        request = wrapRequest(request);
        User user = securityService.getCurrentUser(request);
        if (!user.isDownloadRole()) {
            error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to download files.");
            return null;
        }

        long ifModifiedSince = request.getDateHeader("If-Modified-Since");
        long lastModified = downloadController.getLastModified(request);

        if (ifModifiedSince != -1 && lastModified != -1 && lastModified <= ifModifiedSince) {
            response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
            return null;
        }

        if (lastModified != -1) {
            response.setDateHeader("Last-Modified", lastModified);
        }

        return downloadController.handleRequest(request, response);
    }

    public ModelAndView stream(HttpServletRequest request, HttpServletResponse response) throws Exception {
        request = wrapRequest(request);
        User user = securityService.getCurrentUser(request);
        if (!user.isStreamRole()) {
            error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to play files.");
            return null;
        }

        streamController.handleRequest(request, response);
        return null;
    }

    public ModelAndView hls(HttpServletRequest request, HttpServletResponse response) throws Exception {
        request = wrapRequest(request);
        User user = securityService.getCurrentUser(request);
        if (!user.isStreamRole()) {
            error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to play files.");
            return null;
        }
        hlsController.handleRequest(request, response);
        return null;
    }

    public ModelAndView getCoverArt(HttpServletRequest request, HttpServletResponse response) throws Exception {
        request = wrapRequest(request);
        return coverArtController.handleRequest(request, response);
    }
    
    public ModelAndView getSubtitles(HttpServletRequest request, HttpServletResponse response) throws Exception {
        request = wrapRequest(request);
        return subtitlesController.handleRequest(request, response);
    }

    public void getUser(HttpServletRequest request, HttpServletResponse response) throws Exception {
        request = wrapRequest(request);

        String username = ServletRequestUtils.getRequiredStringParameter(request, "username");

        User currentUser = securityService.getCurrentUser(request);
        if (!username.equals(currentUser.getUsername()) && !currentUser.isAdminRole()) {
            error(request, response, ErrorCode.NOT_AUTHORIZED, currentUser.getUsername() + " is not authorized to get details for other users.");
            return;
        }

        User requestedUser = securityService.getUserByName(username);
        if (requestedUser == null) {
            error(request, response, ErrorCode.NOT_FOUND, "No such user: " + username);
            return;
        }

        XMLBuilder builder = createXMLBuilder(request, response, true);
        List<Attribute> attributes = createAttributesForUser(requestedUser);

        builder.add("user", attributes, true);
        builder.endAll();
        response.getWriter().print(builder);
    }

    private List<Attribute> createAttributesForUser(User user) {
        return Arrays.asList(
                new Attribute("username", user.getUsername()),
                new Attribute("adminRole", user.isAdminRole()),
                new Attribute("settingsRole", user.isSettingsRole()),
                new Attribute("downloadRole", user.isDownloadRole()),
                new Attribute("playlistRole", true), // Since 1.8.0
                new Attribute("streamRole", user.isStreamRole()));
    }

    public void createUser(HttpServletRequest request, HttpServletResponse response) throws Exception {
        request = wrapRequest(request);
        User user = securityService.getCurrentUser(request);
        if (!user.isAdminRole()) {
            error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to create new users.");
            return;
        }

        UserSettingsCommand command = new UserSettingsCommand();
        command.setUsername(ServletRequestUtils.getRequiredStringParameter(request, "username"));
        command.setPassword(decrypt(ServletRequestUtils.getRequiredStringParameter(request, "password")));
        command.setAdminRole(ServletRequestUtils.getBooleanParameter(request, "adminRole", false));
        command.setDownloadRole(ServletRequestUtils.getBooleanParameter(request, "downloadRole", false));
        command.setStreamRole(ServletRequestUtils.getBooleanParameter(request, "streamRole", true));
        command.setSettingsRole(ServletRequestUtils.getBooleanParameter(request, "settingsRole", true));

        XMLBuilder builder = createXMLBuilder(request, response, true).endAll();
        response.getWriter().print(builder);
    }

    public void deleteUser(HttpServletRequest request, HttpServletResponse response) throws Exception {
        request = wrapRequest(request);
        User user = securityService.getCurrentUser(request);
        if (!user.isAdminRole()) {
            error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to delete users.");
            return;
        }

        String username = ServletRequestUtils.getRequiredStringParameter(request, "username");
        securityService.deleteUser(username);

        XMLBuilder builder = createXMLBuilder(request, response, true).endAll();
        response.getWriter().print(builder);
    }

    private HttpServletRequest wrapRequest(final HttpServletRequest request) {
        final String playerId = createPlayerIfNecessary(request);
        return new HttpServletRequestWrapper(request) {
            @Override
            public String getParameter(String name) {
                // Returns the correct player to be used in PlayerService.getPlayer()
                if ("player".equals(name)) {
                    return playerId;
                }

                // Support old style ID parameters.
                if ("id".equals(name)) {
                    return mapId(request.getParameter("id"));
                }

                return super.getParameter(name);
            }
        };
    }

    private String mapId(String id) {
        if (id == null || id.startsWith(CoverArtController.ALBUM_COVERART_PREFIX)
                || id.startsWith(CoverArtController.ARTIST_COVERART_PREFIX) || StringUtils.isNumeric(id)) {
            return id;
        }

        try {
            String path = StringUtil.utf8HexDecode(id);
            MediaElement mediaElement = mediaElementService.getMediaElement(path);
            return String.valueOf(mediaElement.getId());
        } catch (Exception x) {
            return id;
        }
    }

    public static void error(HttpServletRequest request, HttpServletResponse response, ErrorCode code, String message) throws IOException {
        XMLBuilder builder = createXMLBuilder(request, response, false);
        builder.add("error", true,
                new XMLBuilder.Attribute("code", code.getCode()),
                new XMLBuilder.Attribute("message", message));
        builder.end();
        response.getWriter().print(builder);
    }

    private static XMLBuilder createXMLBuilder(HttpServletRequest request, HttpServletResponse response, boolean ok) throws IOException {
        String format = ServletRequestUtils.getStringParameter(request, "f", "xml");
        boolean json = "json".equals(format);
        boolean jsonp = "jsonp".equals(format);
        XMLBuilder builder;

        response.setCharacterEncoding(StringUtil.ENCODING_UTF8);

        if (json) {
            builder = XMLBuilder.createJSONBuilder();
            response.setContentType("application/json");
        } else if (jsonp) {
            builder = XMLBuilder.createJSONPBuilder(request.getParameter("callback"));
            response.setContentType("text/javascript");
        } else {
            builder = XMLBuilder.createXMLBuilder();
            response.setContentType("text/xml");
        }

        builder.preamble(StringUtil.ENCODING_UTF8);
        builder.add("sms-response", false,
                new Attribute("xmlns", "sms-restapi"),
                new Attribute("status", ok ? "ok" : "failed"));
        return builder;
    }

    private String createPlayerIfNecessary(HttpServletRequest request) {
        String username = request.getRemoteUser();
        String clientId = request.getParameter("c");

        List<Player> players = playerService.getPlayersForUserAndClientId(username, clientId);

        // If not found, create it.
        if (players.isEmpty()) {
            Player player = new Player();
            player.setIpAddress(request.getRemoteAddr());
            player.setUsername(username);
            player.setClientId(clientId);
            player.setName(clientId);
            player.setTechnology(PlayerTechnology.EXTERNAL_APPLICATION);
            playerService.createPlayer(player);
            players = playerService.getPlayersForUserAndClientId(username, clientId);
        }

        // Return the player ID.
        return !players.isEmpty() ? players.get(0).getId() : null;
    }

    public void setSettingsService(SettingsService settingsService) {
        this.settingsService = settingsService;
    }

    public void setSecurityService(SecurityService securityService) {
        this.securityService = securityService;
    }

    public void setPlayerService(PlayerService playerService) {
        this.playerService = playerService;
    }

    public void setDownloadController(DownloadController downloadController) {
        this.downloadController = downloadController;
    }

    public void setCoverArtController(CoverArtController coverArtController) {
        this.coverArtController = coverArtController;
    }
    
    public void setSubtitlesController(SubtitlesController subtitlesController) {
        this.subtitlesController = subtitlesController;
    }

    public void setLeftController(LeftController leftController) {
        this.leftController = leftController;
    }

    public void setPlaylistService(PlaylistService playlistService) {
        this.playlistService = playlistService;
    }

    public void setStreamController(StreamController streamController) {
        this.streamController = streamController;
    }

    public void setHlsController(HLSController hlsController) {
        this.hlsController = hlsController;
    }

    public void setHomeController(HomeController homeController) {
        this.homeController = homeController;
    }

    public void setSearchService(SearchService searchService) {
        this.searchService = searchService;
    }

    public void setMediaElementService(MediaElementService mediaElementService) {
        this.mediaElementService = mediaElementService;
    }

    public void setMediaElementDao(MediaElementDao mediaElementDao) {
        this.mediaElementDao = mediaElementDao;
    }

    public static enum ErrorCode {

        GENERIC(0, "A generic error."),
        MISSING_PARAMETER(10, "Required parameter is missing."),
        NOT_AUTHENTICATED(40, "Wrong username or password."),
        NOT_AUTHORIZED(50, "User is not authorized for the given operation."),
        NOT_FOUND(70, "Requested data was not found.");
        private final int code;
        private final String message;

        ErrorCode(int code, String message) {
            this.code = code;
            this.message = message;
        }

        public int getCode() {
            return code;
        }

        public String getMessage() {
            return message;
        }
    }
}
